// ==UserScript==
// @name         Viper Image Downloader
// @version      1.0
// @description  Unified downloader for IMX, Pixhost, Vipr, TurboImageHost, PimpandHost, ImageVenue, ImageBox, ImageBam
// @match        https://viper.to/*
// @match        https://vipergirls.to/*
// @match        https://planetviper.club/*
// @match        https://viperbb.rocks/*
// @match        https://viperkats.eu/*
// @match        https://viperohilia.art/*
// @match        https://viperproxy.org/*
// @match        https://vipervault.link/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      imx.to
// @connect      image.imx.to
// @connect      i.imx.to
// @connect      pixhost.to
// @connect      img*.pixhost.to
// @connect      t*.pixhost.to
// @connect      vipr.im
// @connect      image*.vipr.im
// @connect      i*.vipr.im
// @connect      turboimagehost.com
// @connect      img*.turboimagehost.com
// @connect      up.*.turboimagehost.com
// @connect      turboimg.net
// @connect      pimpandhost.com
// @connect      filesor.com
// @connect      ist*.filesor.com
// @connect      imagevenue.com
// @connect      img*.imagevenue.com
// @connect      cdno-data.imagevenue.com
// @connect      imgbox.com
// @connect      images.imgbox.com
// @connect      images*.imgbox.com
// @connect      imagebam.com
// @connect      images*.imagebam.com
// @connect      thumbs*.imagebam.com
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    maxConcurrent: 20,
    requestTimeout: 15000,
    downloadTimeout: 45000,
    retryAttempts: 2,
    retryDelay: 800
  };

  const activeOperations = new Map();
  const resolutionCache = new Map();

  GM_addStyle(`
    .dl-btn {
      font-size: 11px;
      margin-left: 8px;
      padding: 2px 8px;
      background: #d90d0d;
      color: #fff;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 4px;
      font-weight: 700;
      box-shadow: 0 2px 4px rgba(0,0,0,0.2);
      transition: all 0.18s ease;
      text-align: center;
    }
    .dl-btn[disabled] { opacity: 0.85; cursor: wait; }
    .dl-btn.working { background: #2563eb; }
    .dl-btn.failed { background: #ef4444; cursor: not-allowed; }

    .dl-cancel-btn {
      font-size: 10px;
      margin-left: 4px;
      padding: 0 4px;
      background: #6b7280;
      color: #fff;
      border: none;
      border-radius: 2px;
      cursor: pointer;
      opacity: 0.7;
      transition: opacity 0.2s;
    }
    .dl-cancel-btn:hover { opacity: 1; background: #ef4444; }
    .dl-cancel-btn[hidden] { display: none !important; }

    .dl-overlay {
      position: fixed; inset: 0;
      display: flex; align-items: center; justify-content: center;
      background: rgba(0,0,0,0.6);
      z-index: 2147483646;
      padding: 16px;
    }
    .dl-modal {
      width: 100%; max-width: 1200px; max-height: 92vh; overflow: auto;
      background: #1a1d20; color: #e5e7eb;
      border-radius: 10px; padding: 12px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.5);
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
    }
    .dl-header { display: flex; justify-content: center; align-items: center; gap: 12px; margin-bottom: 14px; }
    .dl-title { font-weight: 700; font-size: 16px; color: #f8fafc; text-align: center; }
    .dl-close {
      position: fixed; right: 18px; bottom: 18px;
      background: #ef4444; color: #fff; border: none;
      width: 100px; height: 100px; border-radius: 50%;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      font-weight: 800; font-size: 40px;
      box-shadow: 0 10px 30px rgba(0,0,0,0.35);
      z-index: 2147483650;
    }
    .dl-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
    @media (max-width:1200px) { .dl-grid { grid-template-columns: repeat(3,1fr); } }
    @media (max-width:768px) { .dl-grid { grid-template-columns: repeat(2,1fr); } }
    .dl-item {
      border: 1px solid rgba(255,255,255,0.06);
      border-radius: 8px; overflow: hidden;
      display: flex; flex-direction: column;
      background: #0f1315;
    }
    .dl-thumb { width: 100%; height: 170px; object-fit: contain; background: #1a1d20; display: block; }
    .dl-actions { display: flex; gap: 6px; padding: 8px; }
    .dl-btn-download { flex: 1; padding: 8px; border-radius: 6px; border: none; font-weight: 700; cursor: pointer; background: #10b981; color: #fff; }
    .dl-btn-failed { flex: 1; padding: 8px; border-radius: 6px; border: none; font-weight: 700; cursor: default; background: #ef4444; color: #fff; }
    .dl-notice {
      position: fixed; top: 12px; right: 12px;
      background: #ef4444; color: #fff;
      padding: 10px 14px; border-radius: 8px;
      z-index: 2147483647;
      box-shadow: 0 8px 30px rgba(0,0,0,0.25);
    }
    .dl-btn-download.success-pulse { animation: dlSuccessPulse 2s ease; }
    @keyframes dlSuccessPulse {
      0%   { box-shadow: 0 0 0 0 rgba(16,185,129,0.7); }
      50%  { box-shadow: 0 0 0 8px rgba(16,185,129,0); }
      100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); }
    }
  `);

  function fixProtocol(url) { if (!url) return url; return url.startsWith('//') ? 'https:' + url : url; }
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  function isImageUrl(url) { if (!url) return false; return /\.(jpg|jpeg|png|gif|webp)$/i.test(String(url).split('?')[0]); }

  function showNotice(msg, timeout = 3000) {
    const n = document.createElement('div');
    n.className = 'dl-notice';
    n.textContent = msg;
    document.body.appendChild(n);
    setTimeout(() => { try { n.remove(); } catch (e) {} }, timeout);
  }

  function openGallery(resolvedPairs, galleryTitle) {
    const overlay = document.createElement('div'); overlay.className = 'dl-overlay';
    const modal = document.createElement('div'); modal.className = 'dl-modal';
    const header = document.createElement('div'); header.className = 'dl-header';
    const title = document.createElement('div'); title.className = 'dl-title';
    const ok = resolvedPairs.filter(p => p.fullUrl).length;
    const fail = resolvedPairs.length - ok;
    title.textContent = `${galleryTitle} — ${ok} resolved${fail ? ' • ' + fail + ' failed' : ''}`;
    const closeBtn = document.createElement('button'); closeBtn.className = 'dl-close'; closeBtn.title = 'Close'; closeBtn.textContent = '✕';
    header.appendChild(title);
    modal.appendChild(header);
    const grid = document.createElement('div'); grid.className = 'dl-grid';

    resolvedPairs.forEach(pair => {
      const item = document.createElement('div'); item.className = 'dl-item';
      const img = document.createElement('img'); img.className = 'dl-thumb'; img.loading = 'lazy';
      img.alt = pair.uniqueId || '';
      img.src = pair.thumbUrl || (pair.fullUrl || '');
      item.appendChild(img);
      const actions = document.createElement('div'); actions.className = 'dl-actions';

      if (pair.fullUrl) {
        img.style.cursor = 'pointer';
        img.addEventListener('click', (e) => {
          e.stopPropagation();
          if (pair.fullUrl) {
            const a = document.createElement('a'); a.href = pair.fullUrl; a.target = '_blank'; a.rel = 'noreferrer';
            document.body.appendChild(a); a.click(); a.remove();
          }
        });
        const dl = document.createElement('button'); dl.className = 'dl-btn-download'; dl.textContent = 'Download';
        dl.dataset.full = pair.fullUrl;
        dl.dataset.page = pair.pageUrl || pair.fullUrl;
        dl.addEventListener('click', (ev) => {
          ev.preventDefault();
          const url = dl.dataset.full;
          const ref = dl.dataset.page || url;
          if (!url) { showNotice('No URL to download'); return; }
          const origText = dl.textContent;
          dl.disabled = true;
          dl.textContent = 'Downloading...';
          GM_xmlhttpRequest({
            method: 'GET', url: url, responseType: 'arraybuffer',
            timeout: CONFIG.downloadTimeout,
            headers: { 'Referer': ref, 'User-Agent': 'Mozilla/5.0' },
            onload(res) {
              if (res.status !== 200) { dl.disabled = false; dl.textContent = origText; showNotice('Download failed HTTP ' + res.status, 4000); return; }
              try {
                const headers = res.responseHeaders || '';
                let ctype = 'application/octet-stream';
                const m = headers.match(/content-type:\s*([^\r\n;]+)/i);
                if (m) ctype = m[1];
                const blob = new Blob([res.response], { type: ctype });
                const blobUrl = URL.createObjectURL(blob);
                const a = document.createElement('a'); a.href = blobUrl; a.download = (url.split('/').pop().split('?')[0]) || 'image';
                document.body.appendChild(a); a.click(); a.remove();
                setTimeout(() => URL.revokeObjectURL(blobUrl), 30000);
                dl.classList.add('success-pulse'); setTimeout(() => dl.classList.remove('success-pulse'), 2000);
                dl.disabled = false; dl.textContent = 'Done'; setTimeout(() => dl.textContent = origText, 900);
              } catch (err) { dl.disabled = false; dl.textContent = origText; showNotice('Download error', 3000); }
            },
            onerror() { dl.disabled = false; dl.textContent = origText; showNotice('Network error', 3000); },
            ontimeout() { dl.disabled = false; dl.textContent = origText; showNotice('Timeout', 3000); }
          });
        });
        actions.appendChild(dl);
      } else {
        const f = document.createElement('button'); f.className = 'dl-btn-failed'; f.textContent = 'Failed'; f.disabled = true;
        actions.appendChild(f);
      }
      item.appendChild(actions);
      grid.appendChild(item);
    });

    modal.appendChild(grid);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    document.body.appendChild(closeBtn);

    function removeOverlay() { try { overlay.remove(); closeBtn.remove(); } catch (e) {} }
    closeBtn.addEventListener('click', removeOverlay);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) removeOverlay(); });
  }

  async function resolveAll(pairs, resolverFn, cacheKeyFn, progressCallback, externalAbortSignal) {
    const results = [];
    const signal = externalAbortSignal || { aborted: false };
    const uncachedPairs = [];

    for (const p of pairs) {
      const cacheKey = cacheKeyFn(p);
      if (resolutionCache.has(cacheKey)) {
        results.push({ ...p, fullUrl: resolutionCache.get(cacheKey), error: null });
      } else {
        uncachedPairs.push(p);
      }
    }
    if (progressCallback && results.length > 0) progressCallback(results.length, pairs.length);

    const batches = [];
    for (let i = 0; i < uncachedPairs.length; i += CONFIG.maxConcurrent) batches.push(uncachedPairs.slice(i, i + CONFIG.maxConcurrent));

    for (let b = 0; b < batches.length; b++) {
      if (signal.aborted) throw new Error('Cancelled');
      const batch = batches[b];
      const promises = batch.map(p => {
        const cacheKey = cacheKeyFn(p);
        return resolverFn(p, signal)
          .then(full => { resolutionCache.set(cacheKey, full); return { ...p, fullUrl: full, error: null }; })
          .catch(err => ({ ...p, fullUrl: null, error: err.message }));
      });
      const res = await Promise.all(promises);
      results.push(...res);
      if (progressCallback) progressCallback(results.length, pairs.length);
      if (signal.aborted) throw new Error('Cancelled');
      if (b < batches.length - 1 && !signal.aborted) await sleep(150);
    }
    return results;
  }

  function addProviderButton(post, providerKey, extractFn, resolverFn, cacheKeyFn, galleryTitle, btnClass, cancelClass, preparePairsFn) {
    try {
      if (post.querySelector('.' + btnClass)) return;
      const rawPairs = extractFn(post);
      if (!rawPairs || rawPairs.length === 0) return;
      const pairs = preparePairsFn ? preparePairsFn(rawPairs) : rawPairs;
      const visibleCount = pairs.length;
      const controls = post.querySelector('span.nodecontrols');
      if (!controls) return;

      const btn = document.createElement('button'); btn.className = 'dl-btn ' + btnClass;
      const arrow = '\u25BC';
      const idleLabel = `${arrow} ${providerKey} (${visibleCount})`;
      btn.textContent = idleLabel;
      btn.title = `${providerKey} — ${visibleCount} images`;
      btn.setAttribute('aria-label', `${providerKey} — ${visibleCount} images`);

      let cancelBtn = controls.querySelector('.' + cancelClass);
      if (!cancelBtn) {
        cancelBtn = document.createElement('button');
        cancelBtn.className = 'dl-cancel-btn ' + cancelClass;
        cancelBtn.textContent = '✕';
        cancelBtn.title = 'Cancel';
        cancelBtn.setAttribute('hidden', '');
        controls.appendChild(cancelBtn);
      }

      const postId = (post.id || `post_${Date.now()}_${Math.floor(Math.random() * 1000)}`) + '_' + providerKey;

      btn.addEventListener('click', async () => {
        if (btn.disabled) return;
        const abortController = new AbortController();
        activeOperations.set(postId, { abortController, button: btn, cancelButton: cancelBtn, originalText: idleLabel });
        btn.disabled = true;
        btn.classList.add('working');
        btn.setAttribute('aria-live', 'polite');
        cancelBtn.removeAttribute('hidden');

        try {
          const resolved = await resolveAll(pairs, resolverFn, cacheKeyFn, (current, total) => {
            btn.textContent = `${arrow} ${providerKey} (${current}/${total})`;
            btn.setAttribute('aria-label', `Resolving ${current} of ${total} images`);
          }, abortController.signal);

          if (abortController.signal.aborted) {
            btn.classList.remove('working'); btn.disabled = false; btn.textContent = idleLabel;
            btn.setAttribute('aria-label', `${providerKey} — ${visibleCount} images`);
            cancelBtn.setAttribute('hidden', ''); activeOperations.delete(postId);
            showNotice('Operation cancelled', 2000);
            return;
          }
          btn.classList.remove('working'); btn.disabled = false; btn.textContent = idleLabel;
          btn.setAttribute('aria-label', `${providerKey} — ${visibleCount} images`);
          cancelBtn.setAttribute('hidden', ''); activeOperations.delete(postId);
          openGallery(resolved, galleryTitle);
        } catch (err) {
          btn.classList.remove('working'); btn.disabled = false; btn.textContent = idleLabel;
          btn.setAttribute('aria-label', `${providerKey} — ${visibleCount} images`);
          cancelBtn.setAttribute('hidden', ''); activeOperations.delete(postId);
          if (err && String(err.message || '').toLowerCase().includes('cancel')) showNotice('Operation cancelled', 2000);
          else showNotice('Resolve error: ' + (err && err.message || 'unknown'), 4000);
        }
      });

      cancelBtn.addEventListener('click', (ev) => {
        try { ev.preventDefault(); ev.stopPropagation(); } catch (e) {}
        const op = activeOperations.get(postId);
        if (op && op.abortController) { try { op.abortController.abort(); } catch (e) {} }
        try {
          btn.classList.remove('working'); btn.disabled = false;
          btn.textContent = `${'\u25BC'} ${providerKey} (${visibleCount})`;
          btn.setAttribute('aria-label', `${providerKey} — ${visibleCount} images`);
          cancelBtn.setAttribute('hidden', ''); activeOperations.delete(postId);
        } catch (e) {}
        showNotice('Operation cancelled', 2000);
      });

      controls.appendChild(btn);
    } catch (e) {}
  }

  function stripExtAndLower(s) {
    if (!s) return '';
    return String(s).split('?')[0].replace(/\.(jpg|jpeg|png|gif|webp)$/i, '').toLowerCase();
  }

  function normalizeImxForKey(u) {
    if (!u) return '';
    try {
      const parsed = new URL(u);
      const host = parsed.hostname.toLowerCase();
      let pathname = parsed.pathname || '/';
      if (host === 't.imx.to' && pathname.startsWith('/t/')) {
        pathname = pathname.replace(/^\/t\//, '/i/');
      } else if (host === 'imx.to' && pathname.startsWith('/t/')) {
        pathname = pathname.replace(/^\/t\//, '/i/');
      } else if (host === 'image.imx.to' && pathname.includes('/u/t/')) {
        pathname = pathname.replace('/u/t/', '/u/i/');
      } else if (host === 'imx.to' && pathname.startsWith('/upload/')) {
        pathname = pathname.replace(/\/upload\/(?:small|medium|large)\//, '/u/i/');
      }
      const parts = pathname.split('/');
      const base = (parts.pop() || parts.pop() || '').split('?')[0];
      return stripExtAndLower(base);
    } catch (e) {
      return stripExtAndLower(String(u).split('/').pop());
    }
  }

  function extractImxLinks(post) {
    const results = [];
    try {
      const anchors = post.querySelectorAll('a[href*="imx.to"], a[href*="image.imx.to"], a[href*="i.imx.to"]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          const imgs = a.querySelectorAll('img'); let thumb = '';
          if (imgs && imgs.length) thumb = imgs[0].src || '';
          results.push({ pageUrl: href, thumbUrl: thumb || href, uniqueId: (href.split('/').pop() || '') });
        } catch (e) {}
      });
      const imgs = post.querySelectorAll('img[src*="imx.to"], img[src*="image.imx.to"], img[src*="i.imx.to"]');
      imgs.forEach(img => {
        try { const src = img.src; if (!src) return; results.push({ pageUrl: src, thumbUrl: src, uniqueId: (src.split('/').pop() || '') }); } catch (e) {}
      });
    } catch (e) {}
    const seen = new Set();
    return results.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function dedupePairsCanonical(pairs) {
    const map = new Map();
    const orderedKeys = [];
    pairs.forEach((p, idx) => {
      const rawUnique = p.uniqueId || '';
      const u = p.pageUrl || p.thumbUrl || '';
      const key = (p.thumbUrl ? normalizeImxForKey(p.thumbUrl) : (rawUnique ? stripExtAndLower(rawUnique) : normalizeImxForKey(u) || String(idx))) || String(idx);
      if (!map.has(key)) {
        orderedKeys.push(key);
        map.set(key, { thumbUrl: p.thumbUrl || null, pageUrl: p.pageUrl || null, uniqueId: rawUnique || null, order: idx });
      } else {
        const ex = map.get(key);
        if (!ex.thumbUrl && p.thumbUrl) ex.thumbUrl = p.thumbUrl;
        if (!ex.pageUrl && p.pageUrl) ex.pageUrl = p.pageUrl;
        if (!ex.uniqueId && rawUnique) ex.uniqueId = rawUnique;
      }
    });
    return orderedKeys.map(k => {
      const v = map.get(k);
      return { thumbUrl: v.thumbUrl, pageUrl: v.pageUrl, uniqueId: v.uniqueId, order: v.order };
    });
  }

  const shortImxRegex = /^https?:\/\/(?:i\.)?imx\.to\/i\/[A-Za-z0-9]+\/?$/i;
  const imgPageRegex = /^https?:\/\/(?:www\.)?imx\.to\/img-[A-Za-z0-9]+\.html$/i;

  function chooseResolveCandidate(p) {
    const page = p.pageUrl || '';
    const thumb = p.thumbUrl || '';
    if (page && (shortImxRegex.test(page) || imgPageRegex.test(page)) && thumb) return thumb;
    return page || thumb;
  }

  function resolveImxLink(pair, abortSignal) {
    const url = pair.resolveUrl || pair.pageUrl || pair.thumbUrl || '';
    if (!url) return Promise.reject(new Error('No URL'));
    if (abortSignal && abortSignal.aborted) return Promise.reject(new Error('Cancelled'));
    try {
      let candidate = url;
      try {
        const parsed = new URL(candidate);
        const host = parsed.hostname.toLowerCase();
        if (host === 't.imx.to') {
          parsed.hostname = 'i.imx.to';
          parsed.pathname = parsed.pathname.replace(/^\/t\//, '/i/');
          candidate = parsed.toString();
        } else if (host === 'image.imx.to' && parsed.pathname.includes('/u/t/')) {
          parsed.pathname = parsed.pathname.replace('/u/t/', '/u/i/');
          candidate = parsed.toString();
        } else if (host === 'imx.to' && parsed.pathname.startsWith('/t/')) {
          parsed.hostname = 'i.imx.to';
          parsed.pathname = parsed.pathname.replace(/^\/t\//, '/i/');
          candidate = parsed.toString();
        } else if (host === 'imx.to' && parsed.pathname.match(/^\/upload\/(small|medium|large)\//)) {
          parsed.hostname = 'image.imx.to';
          parsed.pathname = parsed.pathname.replace(/^\/upload\/(?:small|medium|large)\//, '/u/i/');
          candidate = parsed.toString();
        } else if (host === 'imx.to' && parsed.pathname.startsWith('/upload/')) {
          parsed.hostname = 'image.imx.to';
          parsed.pathname = parsed.pathname.replace(/^\/upload\//, '/u/i/');
          candidate = parsed.toString();
        }
      } catch (e) {}
      candidate = candidate.replace(/_l\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i, '_s.$1$2');
      candidate = fixProtocol(candidate);
      return Promise.resolve(candidate);
    } catch (err) {
      return Promise.reject(err);
    }
  }

  function imxCacheKey(p) { return p.resolveUrl || p.pageUrl || p.thumbUrl || ''; }

  function prepareImxPairs(rawPairs) {
    const merged = dedupePairsCanonical(rawPairs);
    return merged.map(p => ({ ...p, resolveUrl: chooseResolveCandidate(p) }));
  }

  function extractPixhostLinks(post) {
    const links = [];
    try {
      const anchors = post.querySelectorAll('a[href*="pixhost.to"]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          let thumb = '';
          const img = a.querySelector('img[src*="pixhost"]');
          if (img && img.src) thumb = img.src;
          links.push({ pageUrl: href, thumbUrl: thumb, uniqueId: href.split('/').pop() || '' });
        } catch (err) {}
      });
      if (links.length === 0) {
        const imgs = post.querySelectorAll('img[src*="pixhost"]');
        imgs.forEach(img => {
          try { const src = img.src; if (!src) return; links.push({ pageUrl: '', thumbUrl: src, uniqueId: src.split('/').pop() || '' }); } catch (e) {}
        });
      }
    } catch (e) {}
    const seen = new Set();
    return links.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function resolvePixhostLink(pair, abortSignal, retryCount = 0) {
    const pageUrl = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
      if (!pageUrl) { reject(new Error('No page URL to resolve')); return; }
      const abortHandler = () => { try { if (req) req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      let req;
      try {
        req = GM_xmlhttpRequest({
          method: 'GET', url: pageUrl,
          headers: { 'Accept': 'text/html', 'User-Agent': 'Mozilla/5.0' },
          timeout: CONFIG.requestTimeout,
          onload(res) {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (res.status !== 200) {
              if (retryCount < CONFIG.retryAttempts) {
                setTimeout(() => { resolve(resolvePixhostLink(pair, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
              } else reject(new Error(`HTTP ${res.status}`));
              return;
            }
            try {
              const parser = new DOMParser();
              const doc = parser.parseFromString(res.responseText, 'text/html');
              let imageUrl = null;

              const og = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
              if (og) { const c = og.getAttribute('content'); if (c && c.includes('pixhost') && isImageUrl(c)) imageUrl = c; }

              if (!imageUrl) {
                const ids = ['image', 'img', 'picture', 'photo', 'show_image'];
                for (const id of ids) {
                  const el = doc.getElementById(id);
                  if (el) {
                    if (el.tagName && el.tagName.toLowerCase() === 'img' && el.src && el.src.includes('pixhost')) {
                      if (isImageUrl(el.src)) { imageUrl = el.src; break; }
                    } else {
                      const imgInside = el.querySelector && el.querySelector('img[src*="pixhost"]');
                      if (imgInside && imgInside.src && isImageUrl(imgInside.src)) { imageUrl = imgInside.src; break; }
                    }
                  }
                }
              }

              if (!imageUrl) {
                const containers = doc.querySelectorAll('[id*="image"], [id*="img"], [class*="image"], [class*="img"]');
                for (const c of containers) {
                  const img = c.querySelector && c.querySelector('img[src*="pixhost"]');
                  if (img && img.src && isImageUrl(img.src)) { imageUrl = img.src; break; }
                }
              }

              if (!imageUrl) {
                const imgs = doc.querySelectorAll('img[src*="pixhost"]');
                for (const im of imgs) {
                  try {
                    const src = im.src; if (!src) continue;
                    const lower = src.toLowerCase();
                    if (lower.includes('thumb') || lower.includes('_thumb') || lower.includes('_small') || lower.includes('thumbnails')) continue;
                    if (isImageUrl(src)) { imageUrl = src; break; }
                  } catch (e) {}
                }
              }

              if (!imageUrl) {
                const anchors = doc.querySelectorAll('a[href*="pixhost"]');
                for (const a of anchors) {
                  try {
                    const href = a.href; if (!href) continue;
                    if (isImageUrl(href) && href.includes('img') && href.includes('pixhost')) { imageUrl = href; break; }
                  } catch (e) {}
                }
              }

              if (imageUrl) resolve(fixProtocol(imageUrl));
              else {
                if (retryCount < CONFIG.retryAttempts) {
                  setTimeout(() => { resolve(resolvePixhostLink(pair, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
                } else reject(new Error('No image URL found'));
              }
            } catch (err) { reject(err); }
          },
          onerror() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => { resolve(resolvePixhostLink(pair, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Network error'));
          },
          ontimeout() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => { resolve(resolvePixhostLink(pair, abortSignal, retryCount + 1)); }, CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Timeout'));
          }
        });
      } catch (err) { cleanup(); reject(err); }
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function pixhostCacheKey(p) { return p.pageUrl || ''; }

  function extractViprLinks(post) {
    const links = [];
    try {
      const anchors = post.querySelectorAll('a[href*="vipr.im"], a[href*="vipr."]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          let thumb = '';
          const img = a.querySelector('img');
          if (img && img.src) thumb = img.src;
          links.push({ pageUrl: href, thumbUrl: thumb, uniqueId: href.split('/').pop() || '' });
        } catch (err) {}
      });
      if (links.length === 0) {
        const imgs = post.querySelectorAll('img[src*="vipr.im"], img[src*="vipr."]');
        imgs.forEach(img => {
          try { const src = img.src; if (!src) return; links.push({ pageUrl: '', thumbUrl: src, uniqueId: src.split('/').pop() || '' }); } catch (e) {}
        });
      }
    } catch (e) {}
    const seen = new Set();
    return links.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function resolveViprLink(pair, abortSignal, retryCount = 0) {
    const pageUrl = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
      if (!pageUrl) { reject(new Error('No page URL to resolve')); return; }
      const abortHandler = () => { try { if (req) req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      let req;
      try {
        req = GM_xmlhttpRequest({
          method: 'GET', url: pageUrl,
          headers: { 'Accept': 'text/html', 'User-Agent': 'Mozilla/5.0' },
          timeout: CONFIG.requestTimeout,
          onload(res) {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (res.status !== 200) {
              if (retryCount < CONFIG.retryAttempts) {
                setTimeout(() => resolve(resolveViprLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
              } else reject(new Error(`HTTP ${res.status}`));
              return;
            }
            try {
              const parser = new DOMParser();
              const doc = parser.parseFromString(res.responseText, 'text/html');
              let imageUrl = null;
              const og = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
              if (og) { const c = og.getAttribute('content'); if (c && c.includes('vipr') && isImageUrl(c)) imageUrl = c; }
              if (!imageUrl) {
                const ids = ['image', 'img', 'picture', 'photo', 'show_image', 'fullimage'];
                for (const id of ids) {
                  const el = doc.getElementById(id);
                  if (el) {
                    if (el.tagName && el.tagName.toLowerCase() === 'img' && el.src && el.src.includes('vipr')) {
                      if (isImageUrl(el.src)) { imageUrl = el.src; break; }
                    } else {
                      const imgInside = el.querySelector && el.querySelector('img[src*="vipr"]');
                      if (imgInside && imgInside.src && isImageUrl(imgInside.src)) { imageUrl = imgInside.src; break; }
                    }
                  }
                }
              }
              if (!imageUrl) {
                const containers = doc.querySelectorAll('[id*="image"], [id*="img"], [class*="image"], [class*="img"], [class*="photo"]');
                for (const c of containers) {
                  const img = c.querySelector && c.querySelector('img[src*="vipr"]');
                  if (img && img.src && isImageUrl(img.src)) { imageUrl = img.src; break; }
                }
              }
              if (!imageUrl) {
                const imgs = doc.querySelectorAll('img[src*="vipr"]');
                for (const im of imgs) {
                  try {
                    const src = im.src; if (!src) continue;
                    const lower = src.toLowerCase();
                    if (lower.includes('thumb') || lower.includes('_thumb') || lower.includes('_small') || lower.includes('/thumbs/')) continue;
                    if (isImageUrl(src)) { imageUrl = src; break; }
                  } catch (e) {}
                }
              }
              if (!imageUrl) {
                const anchors = doc.querySelectorAll('a[href*="vipr"]');
                for (const a of anchors) {
                  try {
                    const href = a.href; if (!href) continue;
                    if (isImageUrl(href) && href.includes('vipr')) { imageUrl = href; break; }
                  } catch (e) {}
                }
              }
              if (imageUrl) resolve(fixProtocol(imageUrl));
              else {
                if (retryCount < CONFIG.retryAttempts) {
                  setTimeout(() => resolve(resolveViprLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
                } else reject(new Error('No image URL found'));
              }
            } catch (err) { reject(err); }
          },
          onerror() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveViprLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Network error'));
          },
          ontimeout() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveViprLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Timeout'));
          }
        });
      } catch (err) { cleanup(); reject(err); }
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function viprCacheKey(p) { return p.pageUrl || ''; }

  function extractTurboLinks(post) {
    const links = [];
    try {
      const anchors = post.querySelectorAll('a[href*="turboimagehost.com"], a[href*="turboimg"], a[href*="turboimage"]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          let thumb = '';
          const img = a.querySelector('img');
          if (img && img.src) thumb = img.src;
          links.push({ pageUrl: href, thumbUrl: thumb, uniqueId: href.split('/').pop() || '' });
        } catch (err) {}
      });
      if (links.length === 0) {
        const imgs = post.querySelectorAll('img[src*="turboimagehost.com"], img[src*="turboimg"], img[src*="turboimage"]');
        imgs.forEach(img => {
          try { const src = img.src; if (!src) return; links.push({ pageUrl: '', thumbUrl: src, uniqueId: src.split('/').pop() || '' }); } catch (e) {}
        });
      }
    } catch (e) {}
    const seen = new Set();
    return links.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function resolveTurboLink(pair, abortSignal, retryCount = 0) {
    const pageUrl = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
      if (!pageUrl) { reject(new Error('No page URL to resolve')); return; }
      const abortHandler = () => { try { if (req) req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      let req;
      try {
        req = GM_xmlhttpRequest({
          method: 'GET', url: pageUrl,
          headers: { 'Accept': 'text/html', 'User-Agent': 'Mozilla/5.0' },
          timeout: CONFIG.requestTimeout,
          onload(res) {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (res.status !== 200) {
              if (retryCount < CONFIG.retryAttempts) {
                setTimeout(() => resolve(resolveTurboLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
              } else reject(new Error(`HTTP ${res.status}`));
              return;
            }
            try {
              const parser = new DOMParser();
              const doc = parser.parseFromString(res.responseText, 'text/html');
              let imageUrl = null;
              const og = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
              if (og) { const c = og.getAttribute('content'); if (c && c.toLowerCase().includes('turbo') && isImageUrl(c)) imageUrl = c; }
              if (!imageUrl) {
                const ids = ['image', 'img', 'picture', 'photo', 'show_image', 'fullimage', 'main-image', 'image_view'];
                for (const id of ids) {
                  const el = doc.getElementById(id);
                  if (el) {
                    if (el.tagName && el.tagName.toLowerCase() === 'img' && el.src && el.src.includes('turbo')) {
                      if (isImageUrl(el.src)) { imageUrl = el.src; break; }
                    } else {
                      const imgInside = el.querySelector && el.querySelector('img[src*="turbo"]');
                      if (imgInside && imgInside.src && isImageUrl(imgInside.src)) { imageUrl = imgInside.src; break; }
                    }
                  }
                }
              }
              if (!imageUrl) {
                const containers = doc.querySelectorAll('[id*="image"], [id*="img"], [class*="image"], [class*="img"], [class*="photo"], [class*="viewer"]');
                for (const c of containers) {
                  const img = c.querySelector && c.querySelector('img[src*="turbo"]');
                  if (img && img.src && isImageUrl(img.src)) { imageUrl = img.src; break; }
                }
              }
              if (!imageUrl) {
                const imgs = doc.querySelectorAll('img[src*="turbo"]');
                for (const im of imgs) {
                  try {
                    const src = im.src; if (!src) continue;
                    const lower = src.toLowerCase();
                    if (lower.includes('thumb') || lower.includes('_thumb') || lower.includes('_small') || lower.includes('/thumbs/')) continue;
                    if (isImageUrl(src)) { imageUrl = src; break; }
                  } catch (e) {}
                }
              }
              if (!imageUrl) {
                const anchors = doc.querySelectorAll('a[href*="turbo"]');
                for (const a of anchors) {
                  try {
                    const href = a.href; if (!href) continue;
                    if (isImageUrl(href) && href.toLowerCase().includes('turbo')) { imageUrl = href; break; }
                  } catch (e) {}
                }
              }
              if (imageUrl) resolve(fixProtocol(imageUrl));
              else {
                if (retryCount < CONFIG.retryAttempts) {
                  setTimeout(() => resolve(resolveTurboLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
                } else reject(new Error('No image URL found'));
              }
            } catch (err) { reject(err); }
          },
          onerror() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveTurboLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Network error'));
          },
          ontimeout() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveTurboLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Timeout'));
          }
        });
      } catch (err) { cleanup(); reject(err); }
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function turboCacheKey(p) { return p.pageUrl || ''; }

  function stripExtLower(s) {
    if (!s) return '';
    return String(s).split('?')[0].replace(/\.(jpg|jpeg|png|gif|webp)$/i, '').toLowerCase();
  }

  function normalizePmpKey(u) {
    if (!u) return '';
    try {
      const parsed = new URL(u);
      const path = parsed.pathname || '/';
      const base = (path.split('/').pop() || '').split('?')[0];
      return stripExtLower(base);
    } catch (e) {
      return stripExtLower(String(u).split('/').pop());
    }
  }

  function extractPimpLinks(post) {
    const out = [];
    try {
      const anchors = post.querySelectorAll('a[href*="pimpandhost.com/image"], a[href*="filesor.com"], a[href*="pimpandhost.com"]');
      anchors.forEach(a => {
        try {
          const h = a.href; if (!h) return;
          const imgs = a.querySelectorAll('img'); let thumb = '';
          if (imgs && imgs.length) thumb = imgs[0].src || '';
          out.push({ pageUrl: h, thumbUrl: thumb || h, uniqueId: (h.split('/').pop() || '') });
        } catch (e) {}
      });
      const imgs = post.querySelectorAll('img[src*="filesor.com"], img[src*="pimpandhost.com"]');
      imgs.forEach(img => {
        try { const s = img.src; if (!s) return; out.push({ pageUrl: s, thumbUrl: s, uniqueId: (s.split('/').pop() || '') }); } catch (e) {}
      });
    } catch (e) {}
    const seen = new Set();
    return out.filter(item => {
      const k = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(k)) return false;
      seen.add(k);
      return true;
    });
  }

  function dedupePimpPairsCanonical(pairs) {
    const map = new Map();
    const ordered = [];
    pairs.forEach((p, idx) => {
      const raw = p.uniqueId || '';
      const key = p.thumbUrl ? normalizePmpKey(p.thumbUrl) : (raw ? stripExtLower(raw) : normalizePmpKey(p.pageUrl) || String(idx));
      const k = key || String(idx);
      if (!map.has(k)) {
        ordered.push(k);
        map.set(k, { thumbUrl: p.thumbUrl || null, pageUrl: p.pageUrl || null, uniqueId: raw || null, order: idx });
      } else {
        const ex = map.get(k);
        if (!ex.thumbUrl && p.thumbUrl) ex.thumbUrl = p.thumbUrl;
        if (!ex.pageUrl && p.pageUrl) ex.pageUrl = p.pageUrl;
        if (!ex.uniqueId && raw) ex.uniqueId = raw;
      }
    });
    return ordered.map(k => {
      const v = map.get(k);
      return { thumbUrl: v.thumbUrl, pageUrl: v.pageUrl, uniqueId: v.uniqueId, order: v.order };
    });
  }

  const shortPmpPage = /^https?:\/\/(?:www\.)?pimpandhost\.com\/image\/\d+\/?$/i;

  function choosePmpResolveCandidate(p) {
    const page = p.pageUrl || '';
    const thumb = p.thumbUrl || '';
    if (page && shortPmpPage.test(page) && thumb) return thumb;
    return page || thumb;
  }

  function resolvePmpLink(pair, abortSignal) {
    const candidate = pair.resolveUrl || pair.pageUrl || pair.thumbUrl || '';
    if (!candidate) return Promise.reject(new Error('No URL'));
    if (abortSignal && abortSignal.aborted) return Promise.reject(new Error('Cancelled'));
    try {
      let c = candidate;
      try {
        const parsed = new URL(c);
        let path = parsed.pathname || '';
        if (/_s\.(jpg|jpeg|png|gif|webp)$/i.test(path)) {
          path = path.replace(/_s\.(jpg|jpeg|png|gif|webp)$/i, '_l.$1');
          parsed.pathname = path;
          c = parsed.toString();
        } else if (/_m\.(jpg|jpeg|png|gif|webp)$/i.test(path) || /_sm\.(jpg|jpeg|png|gif|webp)$/i.test(path)) {
          path = path.replace(/_(m|sm)\.(jpg|jpeg|png|gif|webp)$/i, '_l.$2');
          parsed.pathname = path;
          c = parsed.toString();
        } else if (/\/upload\/.*_(s|m|sm)\.(jpg|jpeg|png|gif|webp)$/i.test(path)) {
          path = path.replace(/_(s|m|sm)\.(jpg|jpeg|png|gif|webp)$/i, '_l.$2');
          parsed.pathname = path;
          c = parsed.toString();
        }
        try {
          const u2 = new URL(c);
          const p2 = u2.pathname || '';
          u2.pathname = p2.replace(/_(?:s|l|m|sm)\.(jpg|jpeg|png|gif|webp)$/i, '.$1');
          c = u2.toString();
        } catch (e2) {}
      } catch (e) {}
      c = fixProtocol(c);
      return Promise.resolve(c);
    } catch (err) {
      return Promise.reject(err);
    }
  }

  function pimpCacheKey(p) { return p.resolveUrl || p.pageUrl || p.thumbUrl || ''; }

  function preparePimpPairs(rawPairs) {
    const merged = dedupePimpPairsCanonical(rawPairs);
    return merged.map(p => ({ ...p, resolveUrl: choosePmpResolveCandidate(p) }));
  }

  function extractImagevenueLinks(post) {
    const links = [];
    try {
      const anchors = post.querySelectorAll('a[href*="imagevenue.com"][href*="img.php"]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          let thumb = '';
          const img = a.querySelector('img');
          if (img && img.src) thumb = img.src;
          const uniqueId = href.split('image=').pop() || href.split('/').pop() || '';
          links.push({ pageUrl: href, thumbUrl: thumb, uniqueId: uniqueId });
        } catch (err) {}
      });
      if (links.length === 0) {
        const imgs = post.querySelectorAll('img[src*="imagevenue.com"]');
        imgs.forEach(img => {
          try { const src = img.src; if (!src) return; links.push({ pageUrl: '', thumbUrl: src, uniqueId: src.split('/').pop() || '' }); } catch (e) {}
        });
      }
    } catch (e) {}
    const seen = new Set();
    return links.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function resolveImagevenueLink(pair, abortSignal, retryCount = 0) {
    const pageUrl = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
      if (!pageUrl) { reject(new Error('No page URL to resolve')); return; }
      const abortHandler = () => { try { if (req) req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      let req;
      try {
        req = GM_xmlhttpRequest({
          method: 'GET', url: pageUrl,
          headers: { 'Accept': 'text/html', 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.imagevenue.com/' },
          timeout: CONFIG.requestTimeout,
          onload(res) {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (res.status !== 200) {
              if (retryCount < CONFIG.retryAttempts) {
                setTimeout(() => resolve(resolveImagevenueLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
              } else reject(new Error(`HTTP ${res.status}`));
              return;
            }
            try {
              const parser = new DOMParser();
              const doc = parser.parseFromString(res.responseText, 'text/html');
              let imageUrl = null;
              const mainImage = doc.getElementById('main-image');
              if (mainImage && mainImage.src && mainImage.src.includes('imagevenue.com')) {
                imageUrl = mainImage.src;
              }
              if (!mainImage) {
                const thePic = doc.getElementById('thepic');
                if (thePic && thePic.src && thePic.src.includes('imagevenue.com')) {
                  imageUrl = thePic.src;
                }
              }
              if (!imageUrl) {
                const imgTags = doc.querySelectorAll('img[src*="cdno-data.imagevenue.com"]');
                for (const img of imgTags) {
                  const src = img.src;
                  if (src && src.includes('cdno-data.imagevenue.com') && isImageUrl(src)) {
                    imageUrl = src;
                    break;
                  }
                }
              }
              if (!imageUrl) {
                const ogImage = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
                if (ogImage && ogImage.content) {
                  const content = ogImage.content;
                  if (content.includes('imagevenue.com') && isImageUrl(content)) {
                    imageUrl = content;
                  }
                }
              }
              if (!imageUrl) {
                const text = doc.body.textContent || '';
                const regex = /(https?:\/\/[^\s"]*cdno-data\.imagevenue\.com[^\s"]*\.(?:jpg|jpeg|png|gif|webp))/i;
                const match = text.match(regex);
                if (match) imageUrl = match[1];
              }
              if (imageUrl) resolve(fixProtocol(imageUrl));
              else {
                if (retryCount < CONFIG.retryAttempts) {
                  setTimeout(() => resolve(resolveImagevenueLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
                } else reject(new Error('No image URL found'));
              }
            } catch (err) { reject(err); }
          },
          onerror() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveImagevenueLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Network error'));
          },
          ontimeout() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveImagevenueLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Timeout'));
          }
        });
      } catch (err) { cleanup(); reject(err); }
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function imagevenueCacheKey(p) { return p.pageUrl || ''; }

  function extractIboxLinks(post) {
    const links = [];
    try {
      const anchors = post.querySelectorAll('a[href*="imgbox.com"], a[href*="imagebox"], a[href*="imgbox"]');
      anchors.forEach(a => {
        try {
          const href = a.href; if (!href) return;
          let thumb = '';
          const img = a.querySelector('img');
          if (img && img.src) thumb = img.src;
          links.push({ pageUrl: href, thumbUrl: thumb, uniqueId: href.split('/').pop() || '' });
        } catch (err) {}
      });
      if (links.length === 0) {
        const imgs = post.querySelectorAll('img[src*="imgbox.com"], img[src*="imagebox"], img[src*="imgbox"]');
        imgs.forEach(img => {
          try { const src = img.src; if (!src) return; links.push({ pageUrl: '', thumbUrl: src, uniqueId: src.split('/').pop() || '' }); } catch (e) {}
        });
      }
    } catch (e) {}
    const seen = new Set();
    return links.filter(item => {
      const key = (item.pageUrl || '') + '|' + (item.thumbUrl || '');
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function resolveIboxLink(pair, abortSignal, retryCount = 0) {
    const pageUrl = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
      if (!pageUrl) { reject(new Error('No page URL to resolve')); return; }
      const abortHandler = () => { try { if (req) req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      let req;
      try {
        req = GM_xmlhttpRequest({
          method: 'GET', url: pageUrl,
          headers: { 'Accept': 'text/html', 'User-Agent': 'Mozilla/5.0' },
          timeout: CONFIG.requestTimeout,
          onload(res) {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (res.status !== 200) {
              if (retryCount < CONFIG.retryAttempts) {
                setTimeout(() => resolve(resolveIboxLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
              } else reject(new Error(`HTTP ${res.status}`));
              return;
            }
            try {
              const parser = new DOMParser();
              const doc = parser.parseFromString(res.responseText, 'text/html');
              let imageUrl = null;
              const og = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
              if (og) { const c = og.getAttribute('content'); if (c && c.toLowerCase().includes('imgbox') && isImageUrl(c)) imageUrl = c; }
              if (!imageUrl) {
                const ids = ['image', 'img', 'picture', 'photo', 'show_image', 'main-image', 'fullimage'];
                for (const id of ids) {
                  const el = doc.getElementById(id);
                  if (el) {
                    if (el.tagName && el.tagName.toLowerCase() === 'img' && el.src && el.src.includes('imgbox')) {
                      if (isImageUrl(el.src)) { imageUrl = el.src; break; }
                    } else {
                      const imgInside = el.querySelector && el.querySelector('img[src*="imgbox"]');
                      if (imgInside && imgInside.src && isImageUrl(imgInside.src)) { imageUrl = imgInside.src; break; }
                    }
                  }
                }
              }
              if (!imageUrl) {
                const containers = doc.querySelectorAll('[id*="image"], [id*="img"], [class*="image"], [class*="img"], [class*="photo"], [class*="viewer"]');
                for (const c of containers) {
                  const img = c.querySelector && c.querySelector('img[src*="imgbox"]');
                  if (img && img.src && isImageUrl(img.src)) { imageUrl = img.src; break; }
                }
              }
              if (!imageUrl) {
                const imgs = doc.querySelectorAll('img[src*="imgbox"], img[src*="imagebox"]');
                for (const im of imgs) {
                  try {
                    const src = im.src; if (!src) continue;
                    const lower = src.toLowerCase();
                    if (lower.includes('thumb') || lower.includes('_thumb') || lower.includes('_small') || lower.includes('/thumbs/')) continue;
                    if (isImageUrl(src)) { imageUrl = src; break; }
                  } catch (e) {}
                }
              }
              if (!imageUrl) {
                const anchors = doc.querySelectorAll('a[href*="imgbox"], a[href*="imagebox"]');
                for (const a of anchors) {
                  try {
                    const href = a.href; if (!href) continue;
                    if (isImageUrl(href) && href.toLowerCase().includes('imgbox')) { imageUrl = href; break; }
                  } catch (e) {}
                }
              }
              if (imageUrl) resolve(fixProtocol(imageUrl));
              else {
                if (retryCount < CONFIG.retryAttempts) {
                  setTimeout(() => resolve(resolveIboxLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
                } else reject(new Error('No image URL found'));
              }
            } catch (err) { reject(err); }
          },
          onerror() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveIboxLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Network error'));
          },
          ontimeout() {
            cleanup();
            if (abortSignal && abortSignal.aborted) { reject(new Error('Cancelled')); return; }
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveIboxLink(pair, abortSignal, retryCount + 1)), CONFIG.retryDelay * Math.pow(2, retryCount));
            } else reject(new Error('Timeout'));
          }
        });
      } catch (err) { cleanup(); reject(err); }
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function iboxCacheKey(p) { return p.pageUrl || ''; }

  function extractImagebamLinks(post) {
    const links = new Set();
    try {
      const anchorTags = post.querySelectorAll('a[href*="imagebam.com"]');
      anchorTags.forEach(a => {
        try {
          const href = a.href;
          if (href && /imagebam\.com/i.test(href)) {
            if (href.includes('/view/') || href.includes('/image/')) links.add(href);
          }
        } catch (e) {}
      });
      const imgTags = post.querySelectorAll('img');
      imgTags.forEach(img => {
        try {
          const src = img.src || '';
          if (/imagebam\.com/i.test(src) || /thumb|thumbnail|thumbnails/i.test(src)) {
            const anchor = img.closest('a[href*="imagebam.com"]');
            if (anchor && anchor.href) {
              const href = anchor.href;
              if (href.includes('/view/') || href.includes('/image/')) links.add(href);
            }
          }
        } catch (e) {}
      });
    } catch (e) {}
    return Array.from(links).map(href => {
      const el = document.querySelector(`a[href="${href}"] img`);
      return { pageUrl: href, thumbUrl: el ? el.src : '', uniqueId: href.split('/').pop() || '' };
    });
  }

  function resolveImagebamLink(pair, abortSignal, retryCount = 0) {
    const link = pair.pageUrl;
    return new Promise((resolve, reject) => {
      if (abortSignal && abortSignal.aborted) return reject(new Error('Cancelled'));
      const abortHandler = () => { try { req.abort(); } catch (e) {} };
      const cleanup = () => { try { if (abortSignal) abortSignal.removeEventListener('abort', abortHandler); } catch (e) {} };
      const req = GM_xmlhttpRequest({
        method: 'GET',
        url: link,
        headers: { 'Accept': 'text/html', 'Referer': 'https://www.imagebam.com/', 'User-Agent': 'Mozilla/5.0' },
        timeout: CONFIG.requestTimeout,
        onload(res) {
          cleanup();
          if (abortSignal && abortSignal.aborted) return reject(new Error('Cancelled'));
          if (res.status !== 200) {
            if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveImagebamLink(pair, abortSignal, retryCount + 1)),
                CONFIG.retryDelay * Math.pow(2, retryCount));
            } else {
              reject(new Error(`HTTP ${res.status}`));
            }
            return;
          }
          try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(res.responseText, 'text/html');
            let imageUrl = null;
            const og = doc.querySelector('meta[property="og:image"], meta[name="og:image"]');
            if (og) {
              const c = og.getAttribute('content');
              if (c && /imagebam\.com/i.test(c) && isImageUrl(c)) {
                imageUrl = fixProtocol(c);
              }
            }
            if (!imageUrl) {
              const imgTags = doc.querySelectorAll('img[src]');
              for (const img of imgTags) {
                try {
                  const src = img.src;
                  if (src && /imagebam\.com/i.test(src) && isImageUrl(src)) {
                    if (/_o\.|_orig|original/i.test(src)) { imageUrl = fixProtocol(src); break; }
                    if (!imageUrl) imageUrl = fixProtocol(src);
                  }
                } catch (e) {}
              }
            }
            if (!imageUrl) {
              const anchors = doc.querySelectorAll('a[href]');
              for (const a of anchors) {
                try {
                  const href = a.href;
                  if (href && /imagebam\.com/i.test(href) && isImageUrl(href)) {
                    if (/_o\.|_orig|original/i.test(href)) { imageUrl = fixProtocol(href); break; }
                    if (!imageUrl) imageUrl = fixProtocol(href);
                  }
                } catch (e) {}
              }
            }
            if (imageUrl) resolve(imageUrl);
            else if (retryCount < CONFIG.retryAttempts) {
              setTimeout(() => resolve(resolveImagebamLink(pair, abortSignal, retryCount + 1)),
                CONFIG.retryDelay * Math.pow(2, retryCount));
            } else {
              reject(new Error('No image URL found'));
            }
          } catch (err) {
            reject(err);
          }
        },
        onerror() {
          cleanup();
          if (retryCount < CONFIG.retryAttempts) {
            setTimeout(() => resolve(resolveImagebamLink(pair, abortSignal, retryCount + 1)),
              CONFIG.retryDelay * Math.pow(2, retryCount));
          } else reject(new Error('Network error'));
        },
        ontimeout() {
          cleanup();
          if (retryCount < CONFIG.retryAttempts) {
            setTimeout(() => resolve(resolveImagebamLink(pair, abortSignal, retryCount + 1)),
              CONFIG.retryDelay * Math.pow(2, retryCount));
          } else reject(new Error('Timeout'));
        }
      });
      if (abortSignal) abortSignal.addEventListener('abort', abortHandler);
    });
  }

  function imagebamCacheKey(p) { return p.pageUrl || ''; }

  function scan() {
    document.querySelectorAll('li.postbitlegacy').forEach(post => {
      try {
        // IMX
        addProviderButton(
          post,
          'IMX',
          extractImxLinks,
          resolveImxLink,
          imxCacheKey,
          'IMX Gallery',
          'dl-btn-imx',
          'dl-cancel-imx',
          prepareImxPairs
        );

        // Pixhost
        addProviderButton(
          post,
          'PIX',
          extractPixhostLinks,
          resolvePixhostLink,
          pixhostCacheKey,
          'Pixhost Gallery',
          'dl-btn-pixhost',
          'dl-cancel-pixhost',
          null
        );

        // Vipr
        addProviderButton(
          post,
          'VPR',
          extractViprLinks,
          resolveViprLink,
          viprCacheKey,
          'Vipr Gallery',
          'dl-btn-vipr',
          'dl-cancel-vipr',
          null
        );

        // TurboImageHost
        addProviderButton(
          post,
          'TRB',
          extractTurboLinks,
          resolveTurboLink,
          turboCacheKey,
          'TurboImageHost Gallery',
          'dl-btn-turbo',
          'dl-cancel-turbo',
          null
        );

        // PimpandHost
        addProviderButton(
          post,
          'P&H',
          extractPimpLinks,
          resolvePmpLink,
          pimpCacheKey,
          'PimpandHost Gallery',
          'dl-btn-pimp',
          'dl-cancel-pimp',
          preparePimpPairs
        );

        // ImageVenue
        addProviderButton(
          post,
          'IVN',
          extractImagevenueLinks,
          resolveImagevenueLink,
          imagevenueCacheKey,
          'ImageVenue Gallery',
          'dl-btn-imagevenue',
          'dl-cancel-imagevenue',
          null
        );

        // ImageBox
        addProviderButton(
          post,
          'IBX',
          extractIboxLinks,
          resolveIboxLink,
          iboxCacheKey,
          'ImageBox Gallery',
          'dl-btn-ibox',
          'dl-cancel-ibox',
          null
        );

        // ImageBam
        addProviderButton(
          post,
          'IBM',
          extractImagebamLinks,
          resolveImagebamLink,
          imagebamCacheKey,
          'ImageBam Gallery',
          'dl-btn-imagebam',
          'dl-cancel-imagebam',
          null
        );
      } catch (e) {}
    });
  }

  function init() {
    scan();
    let scanTimeout = null;
    const mo = new MutationObserver(() => {
      if (scanTimeout) clearTimeout(scanTimeout);
      scanTimeout = setTimeout(scan, 150);
    });
    mo.observe(document.body, { childList: true, subtree: true });
    setTimeout(scan, 500);
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();

})();